/**
 * 02/dic/2011 - fabio.melillo@gmail.com
 */
package org.unisannio.server;


import it.unipr.ce.dsg.s2p.message.BasicMessage;
import it.unipr.ce.dsg.s2p.org.json.JSONObject;
import it.unipr.ce.dsg.s2p.sip.Address;

import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.unisannio.entity.Peer;
import org.unisannio.entity.Service;
import org.unisannio.remote.SuperPeer;
import org.unisannio.util.Converter;
import org.unisannio.util.ServiceInfo;
import org.unisannio.util.ServiceInfo.ServiceDescriptor;

/**
 * @author fabio
 *
 */
public class CustomServer extends SuperPeer {
	
	private static final String KEY = String.valueOf(GregorianCalendar.getInstance().getTimeInMillis());
	private static CustomServer me=null;
	private Session session;
	private long expiry=1000*60; //1 minuto
	private final String[] timezone=TimeZone.getAvailableIDs(0);
	
	private CustomServer(String pathConfig, String key, String peerName,
			int peerPort, Session session,int cleanup) {
		super(pathConfig, key, peerName, peerPort);
		this.session=session;
		Thread clean=new CleanUp(this,cleanup);
		clean.start();
	}
	
	public static CustomServer getInstance(Session session,int cleanup) {
		if (me==null){
			int peerPort=2222;
			String name="superPeer";
			me = new CustomServer(null, KEY, name, peerPort,session,cleanup);
		}
		return me;
	}
	
	protected void publish(ServiceInfo sinfo) {
		boolean changed=this.reconnect(sinfo);
		String mac=sinfo.getMac();
		ArrayList<ServiceDescriptor> des = sinfo.getDescriptors();
		if (des!=null){
			int size=des.size();
			for (int i=0;i<size;i++){
				if (this.setService(mac, des.get(i),1,0) || changed) //per ogni nuovo servizio pubblicato cerca i mapping con i subscribed
						this.findMatch(des.get(i), mac);
			}
		}
	}


	private void findMatch(ServiceDescriptor serviceDescriptor, String mac) {
		ArrayList<ServiceInfo> subscribed = this.checkServiceByUri(serviceDescriptor,false,mac);
		if (subscribed.size()>0)
			this.sendNotify(subscribed,this.getServiceMac(serviceDescriptor,mac));
	}

	@Override
	protected void subscribe(ServiceInfo sinfo) {
		this.reconnect(sinfo);
		ArrayList<ServiceDescriptor> des = sinfo.getDescriptors();
		ArrayList<ServiceInfo> sinfoList=new ArrayList<ServiceInfo>();
		if (des!=null){
			for (ServiceDescriptor current : des) {
				this.setService(sinfo.getMac(), current, 0, 1);
				sinfoList=this.checkServiceByUri(current,true,sinfo.getMac());
			}
		}
		this.sendNotify(null, sinfoList);
		
	}

	private void sendNotify(ArrayList<ServiceInfo> peers, ArrayList<ServiceInfo> providers) {
		if (providers.size()>0){
		Converter converter=new Converter();
		BasicMessage basicMsg=converter.marshal(providers, Converter.NOTIFY);
		//setting expiring date
		GregorianCalendar greg=new GregorianCalendar(TimeZone.getTimeZone(timezone[0]));
		long now=greg.getTimeInMillis();
		long timestamp=now+expiry;
		basicMsg.setTimestamp(timestamp);
		printNow();
		if (peers==null){
			System.out.println("invio a [SENDER] "+sender.toString() +" "+ basicMsg.toString());
			this.send(sender, basicMsg);
		}
		else
			for (ServiceInfo peer : peers) {
				Address target=new Address(peer.getMac(),peer.getIp(),Integer.parseInt(peer.getPort()));
				System.out.println("invio a "+target+ " " + basicMsg.toString());
//				System.out.println("invio a "+target+ " l'info di "+ providers.get(0).getAddress());
				this.send(target, basicMsg);
			}
		}
	}
	
	/**
	 * 
	 * @param descriptor
	 * @param published - true if you're finding out an already published service [false for the subscribed ones]
	 * @return
	 */
	private ArrayList<ServiceInfo> checkServiceByUri(ServiceDescriptor descriptor, boolean published, String butMac) {
		Transaction tx = session.beginTransaction();
		String queryString;
		if (published)
			queryString="from Service where uri= :URI and published=1";
		else 
			queryString="from Service where uri= :URI and subscribed=1";
		Query query = session.createQuery(queryString);  
		query.setString("URI", descriptor.getUri());
		ArrayList<Service> serviceList=(ArrayList<Service>) query.list();
		ArrayList<ServiceInfo> sinfoList=new ArrayList<ServiceInfo>();
		if (serviceList.size()>0){
			for(int i=0;i<serviceList.size();i++){
				Service currentDesc=serviceList.get(i);
				queryString="from Peer where mac= :MAC AND not(mac= :SENDERMAC)"; //tutti tranne il mio
				query = session.createQuery(queryString);  
				query.setString("MAC", currentDesc.getMac());
				query.setString("SENDERMAC", butMac);
				Object queryResult = query.uniqueResult();
				if (queryResult!=null){
					Peer currentPeer = (Peer) queryResult;
					ServiceInfo sinfo=new ServiceInfo(currentPeer.getMac(),currentPeer.getIp(),currentPeer.getPort());
					sinfo.addDescriptor(currentDesc.getUri(), currentDesc.getTags(), currentDesc.getSchema(), currentDesc.getType());
					sinfoList.add(sinfo);
				}
			}
		}
		session.flush();
		tx.commit();
		
		String esito= sinfoList.size()>0 ? "FOUNDED "+sinfoList.size() : "NO FOUNDED";
		
		String tipo = published ? " PUBLISHED" : " SUBSCRIBED";
		System.out.println(esito + tipo);
		for (ServiceInfo serviceInfo : sinfoList) {
			System.out.println(serviceInfo.getAddress());
		}
		System.out.println("[EOF]\n");
		return sinfoList;
	}
	
	
	private ArrayList<ServiceInfo> getServiceMac(ServiceDescriptor descriptor, String theMac) {
		Transaction tx = session.beginTransaction();
		String queryString;
		queryString="from Service where uri= :URI and published=1";
		Query query = session.createQuery(queryString);  
		query.setString("URI", descriptor.getUri());
		ArrayList<Service> serviceList=(ArrayList<Service>) query.list();
		ArrayList<ServiceInfo> sinfoList=new ArrayList<ServiceInfo>();
		if (serviceList.size()>0){
			for(int i=0;i<serviceList.size();i++){
				Service currentDesc=serviceList.get(i);
				queryString="from Peer where mac= :MAC)"; //tutti tranne il mio
				query = session.createQuery(queryString);  
				query.setString("MAC", currentDesc.getMac());
				Object queryResult = query.uniqueResult();
				if (queryResult!=null){
					Peer currentPeer = (Peer) queryResult;
					
					ServiceInfo sinfo=new ServiceInfo(currentPeer.getMac(),currentPeer.getIp(),currentPeer.getPort());
					if (currentPeer.getMac().equals(theMac))
						//qui i tags devono contenere NEW
						sinfo.addDescriptor(currentDesc.getUri(), currentDesc.getTags().concat(ServiceInfo.NEWTAG), currentDesc.getSchema(), currentDesc.getType());
					else
						sinfo.addDescriptor(currentDesc.getUri(), currentDesc.getTags(), currentDesc.getSchema(), currentDesc.getType());
					sinfoList.add(sinfo);
				}
			}
		}
		session.flush();
		tx.commit();
		
		String esito= sinfoList.size()>0 ? "FOUNDED "+sinfoList.size() : "NO FOUNDED";
		
		String tipo = " PUBLISHED";
		System.out.println(esito + tipo);
		for (ServiceInfo serviceInfo : sinfoList) {
			System.out.println(serviceInfo.getAddress());
		}
		System.out.println("[EOF]\n");
		return sinfoList;
	}

	@Override
	protected boolean reconnect(ServiceInfo sinfo) {
		printNow();
		boolean changed=false;
		org.unisannio.entity.Peer ePeer=new org.unisannio.entity.Peer();
		ePeer.setIp(sinfo.getIp());
		ePeer.setMac(sinfo.getMac());
		ePeer.setPort(sinfo.getPort());
		Transaction tx = session.beginTransaction();
		String queryString="from Peer where mac= :MAC";
		Query query = session.createQuery(queryString);  
		query.setString("MAC", ePeer.getMac());  
		Object queryResult = query.uniqueResult();  
		Peer currentPeer = (Peer) queryResult;
		if (currentPeer!=null){
				currentPeer.setIp(ePeer.getIp());
				currentPeer.setPort(ePeer.getPort());
				session.update(currentPeer);
				System.out.println("reconnected: "+ePeer);
				changed=true;
		} else {
			session.merge(ePeer);
			System.out.println("saved: "+ePeer);
			changed=true;
		}
//		session.flush();
		tx.commit();
		return changed;
	}

	@Override
	protected void retractFromAll(ServiceInfo sinfo) {
		this.reconnect(sinfo);
		String mac=sinfo.getMac();
		Transaction tx = session.beginTransaction();
		String delete = "delete from Service where mac= :MAC and published=1";
		Query query = session.createQuery(delete);  
		query.setString("MAC", mac);
		int rows=query.executeUpdate();
		session.flush();
		tx.commit();
		System.out.println(sinfo.getAddress()+ " RETRACTED from "+ rows + " rows");
	}

	@Override
	protected void retractFromService(ServiceInfo sinfo) {
		this.reconnect(sinfo);
		String mac=sinfo.getMac();
		ArrayList<ServiceDescriptor> des = sinfo.getDescriptors();
		Transaction tx = session.beginTransaction();
		for (ServiceDescriptor serviceDescriptor : des) {
			String uri=serviceDescriptor.getUri();
			String delete = "delete from Service where mac= :MAC and uri= :URI and published=1";
			Query query = session.createQuery(delete);  
			query.setString("MAC", mac);
			query.setString("URI", uri);
			int rows=query.executeUpdate();
			System.out.println(sinfo.getAddress()+ " RETRACTED from "+ rows + " rows");
		}
		session.flush();
		tx.commit();
		session.clear();
	}

	@Override
	protected void onDeliveryMsgFailure(String arg0, Address address, String arg2) {
		printNow();
		System.out.println("FAILURE "+address.toString());
		String mac=address.getUserName();
		Transaction tx=session.beginTransaction();
		String delete = "delete from Peer where mac= :MAC";
		Query query = session.createQuery(delete);  
		query.setString("MAC", mac);
		int rows=query.executeUpdate();
		 delete = "delete from Service where _mac= :MAC";
		query = session.createQuery(delete);  
		query.setString("MAC", mac);
		rows=query.executeUpdate();
		session.flush();
		tx.commit();
		session.clear();
		System.out.println(address+ " NOT REACHABLE -> DELETED");
	}

	@Override
	protected void onDeliveryMsgSuccess(String arg0, Address address, String arg2) {
		printNow();
		System.out.println("SUCCESS "+address.toString());
	}
	
	@Override
	protected void onReceivedJSONMsg(JSONObject jsonMsg, Address sender) {
		super.onReceivedJSONMsg(jsonMsg, sender);
	}

	@Override
	protected void unsubscribeFromAll(ServiceInfo sinfo) {
		this.reconnect(sinfo);
		String mac=sinfo.getMac();
		Transaction tx = session.beginTransaction();
		String delete = "delete from Service where mac= :MAC and subscribed=1";
		Query query = session.createQuery(delete);  
		query.setString("MAC", mac);
		int rows=query.executeUpdate();
		session.flush();
		tx.commit();
		System.out.println(sinfo.getAddress()+ " UNSUBSCRIBE from "+ rows + " rows");
	}

	@Override
	protected void unsubscribeFromService(ServiceInfo sinfo) {
		this.reconnect(sinfo);
		String mac=sinfo.getMac();
		ArrayList<ServiceDescriptor> des = sinfo.getDescriptors();
		Transaction tx = session.beginTransaction();
		for (ServiceDescriptor serviceDescriptor : des) {
			String uri=serviceDescriptor.getUri();
			String delete = "delete from Service where mac= :MAC and uri= :URI and subscribed=1";
			Query query = session.createQuery(delete);  
			query.setString("MAC", mac);
			query.setString("URI", uri);
			int rows=query.executeUpdate();
			System.out.println(sinfo.getAddress()+ " UNSUBSCRIBE from "+ rows + " rows");
		}
		session.flush();
		tx.commit();
	}
	
	protected boolean setService(String mac, ServiceDescriptor desc, int published, int subscribed){
		Transaction tx = session.beginTransaction();
		String queryString="from Service where _mac= :MAC and uri= :URI";
		Query query = session.createQuery(queryString);  
		query.setString("MAC", mac);
		query.setString("URI", desc.getUri());
		Object queryResult = query.uniqueResult();  
		Service currentService = (Service) queryResult;
		if(currentService==null){
			Service service=new Service(mac,desc);
			service.setPublished(published);
			service.setSubscribed(subscribed);
//			session.flush();
			session.merge(service);
			tx.commit();
			System.out.println("saved: "+service);
			
		}
		else{
			if(published==1)	currentService.setPublished(published);
			else	{
				currentService.setSubscribed(subscribed);
				session.merge(currentService);
				tx.commit();
				
				return false;
			}
			session.merge(currentService);
			tx.commit();
			System.out.println("saved: "+currentService);
		
		}
		return true;
		
		
		
//		if (currentService==null) {
//			Service service=new Service(mac,desc);
//			service.setPublished(published);
//			service.setSubscribed(subscribed);
////			session.flush();
//			session.merge(service);
//			tx.commit();
//			System.out.println("saved: "+service);
//			return true;
//		}
//		else System.out.println("already present: "+currentService);
//		return false;
	}

	public void checkRecheability() {
		Transaction tx = session.beginTransaction();
		String querystr="from Peer";
		Query query = session.createQuery(querystr);
		ArrayList<Peer> peers=(ArrayList<Peer>) query.list();
		session.flush();
		tx.commit();
		BasicMessage msg=new BasicMessage(null,null);
		Address address;
		for (Peer peer : peers) {
			address=new Address(peer.getMac(),peer.getIp(),Integer.parseInt(peer.getPort()));
			this.send(address,msg);
		}
	}
	
	public long newExpiry(int minutes){
		expiry = 1000*60*minutes;
		return expiry;
	}
	
	private void printNow(){
		System.out.println("\nChecked at: "+new Date(new GregorianCalendar(TimeZone.getTimeZone(timezone[0])).getTimeInMillis()));
	}

}
